/*
 * Copyright 2007 National Bioinformatics Network
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dgbgui.gui.widgets.layouts;

import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import dgbgui.gui.widgets.HasEdgePort;
import dgbgui.gui.widgets.Widget;
import dgbgui.modeldocument.primitives.DefaultEdgeWithWidget;
import edu.umd.cs.piccolo.activities.PActivity;


public class FruchtermanReingoldLayoutActivity extends PActivity {

    public FruchtermanReingoldLayoutActivity(GraphLayout layout, long duration, long stepTime) {
        super(duration,stepTime);

        m_layout = layout;

        int index = 0;
        Set<Widget> vertices = m_layout.vertexSet();
        for ( Widget vertex : vertices ) {
            m_positions.put(vertex,new Point2D.Double(
                    Math.random()*100,
                    Math.random()*100));
            ++index;
            vertex.setOffset(m_positions.get(vertex));
            
            m_velocities.put(vertex, new Point2D.Double(0,0));
        }
        
        m_energyThreshold = vertices.size() * ENERGY_THRESHOLD_PER_VERTEX;
    }
    
    public void calcPositions(Widget vertex) {
        double deltaX = Math.min(m_velocities.get(vertex).getX()*TIMESTEP,MAXDELTA);
        double deltaY = Math.min(m_velocities.get(vertex).getY()*TIMESTEP,MAXDELTA);

        double newX = m_positions.get(vertex).getX() + deltaX;
        double newY = m_positions.get(vertex).getY() + deltaY;
        
        m_positions.put(vertex,new Point2D.Double(newX,newY));

    }
    
    public void calcRepulsion(Widget vertex1, Widget vertex2) {
        assert(vertex1 != vertex2);
        double dvx = 0.0;
        double dvy = 0.0;
        
        double xDistance = m_positions.get(vertex2).getX() - m_positions.get(vertex1).getX();
        double yDistance = m_positions.get(vertex2).getY() - m_positions.get(vertex1).getY();

        double deltaLength = Math.max(EPSILON, m_positions.get(vertex1).distance(m_positions.get(vertex2)));
        double force = Math.min(FORCEMAX,(REPULFORCECONSTANT*REPULFORCECONSTANT) / deltaLength );

        dvx += -xDistance/deltaLength*force;
        dvy += -yDistance/deltaLength*force;

        m_velocities.put(vertex1, new Point2D.Double(
                m_velocities.get(vertex1).getX() + dvx,
                m_velocities.get(vertex1).getY() + dvy));
        
        m_velocities.put(vertex2, new Point2D.Double(
                m_velocities.get(vertex2).getX() - dvx,
                m_velocities.get(vertex2).getY() - dvy));
        
    }
    
    /**
     * We don't want vertices to overlap, so there is another respulsive 
     * delta position calculated to ensure this
     * @param vertex
     * @param other
     */
    public void calculateOverlapRepulsion(Widget vertex, Widget other) {
        assert(vertex != other);
        /// Extra force for intersect
        if ( vertex.getFullBounds().intersects(other.getFullBounds()) )  {
            
            double xDistance = m_positions.get(other).getX() - m_positions.get(vertex).getX();
            double yDistance = m_positions.get(other).getY() - m_positions.get(vertex).getY();
            
            Rectangle2D intersectRect = new Rectangle2D.Double(0,0,0,0);
            Rectangle2D paddedFrom = new Rectangle2D.Double(
                    vertex.getFullBounds().getX() - VERTEX_PADDING,
                    vertex.getFullBounds().getY() - VERTEX_PADDING,
                    vertex.getFullBounds().getWidth() + VERTEX_PADDING*2,
                    vertex.getFullBounds().getHeight() + VERTEX_PADDING*2);
            Rectangle2D paddedTo = new Rectangle2D.Double(
                    other.getFullBounds().getX() - VERTEX_PADDING,
                    other.getFullBounds().getY() - VERTEX_PADDING,
                    other.getFullBounds().getWidth() + VERTEX_PADDING*2,
                    other.getFullBounds().getHeight() + VERTEX_PADDING*2);
                    
            
            Rectangle2D.intersect(paddedFrom, paddedTo, intersectRect);
            
            double intersectDiagonal = Math.sqrt(
                    intersectRect.getWidth()*intersectRect.getWidth() +
                    intersectRect.getHeight()*intersectRect.getHeight());
                        
            double force2 = FORCEMAX;//Math.min(FORCEMAX,intersectDiagonal*intersectDiagonal*OVERLAPREPULFORCECONSTANT );
            double dvx = -intersectRect.getWidth()/intersectDiagonal*force2;
            double dvy = -intersectRect.getHeight()/intersectDiagonal*force2;
            
            if ( xDistance < 0 ) {
                dvx = -dvx;
            }
            if ( yDistance < 0 ) {
                dvy = -dvy;
            }
            
            m_velocities.put(vertex, new Point2D.Double(
                    m_velocities.get(vertex).getX() + dvx,
                    m_velocities.get(vertex).getY() + dvy));
            
            m_velocities.put(other, new Point2D.Double(
                    m_velocities.get(vertex).getX() - dvx,
                    m_velocities.get(vertex).getY() - dvy));
        }
        
        
    }
    
    public void calcAttraction(Widget vertex1,Widget vertex2) {

        Rectangle2D v1Rect = new Rectangle2D.Double(
                m_positions.get(vertex1).getX(),
                m_positions.get(vertex1).getY(),
                vertex1.getFullBounds().getWidth(),
                vertex1.getFullBounds().getHeight());
        
        Rectangle2D v2Rect = new Rectangle2D.Double(
                m_positions.get(vertex2).getX(),
                m_positions.get(vertex2).getY(),
                vertex2.getFullBounds().getWidth(),
                vertex2.getFullBounds().getHeight());
        
        if (!( v1Rect.intersects(v2Rect))) {

            double xDelta = m_positions.get(vertex2).getX() - m_positions.get(vertex1).getX();
            double yDelta = m_positions.get(vertex2).getY() - m_positions.get(vertex1).getY();
            
            // Subtract the overlap
            Rectangle2D intersectRect = new Rectangle2D.Double(0,0,0,0);
            Rectangle2D.intersect(v1Rect, v2Rect, intersectRect);
            
            // Subtract the width from the delta
            if ( intersectRect.getWidth() > 0 ) {
                if ( xDelta > 0 ) {
                    xDelta -= intersectRect.getWidth();
                } else {
                    xDelta += intersectRect.getWidth();
                }
            }
            
            // Subtract the height from the delta
            if ( intersectRect.getHeight() > 0 ) {
                if ( yDelta > 0 ) {
                    yDelta -= intersectRect.getHeight();
                } else {
                    yDelta += intersectRect.getHeight();
                }
            }
            

            double deltaLength = Math.max(EPSILON, Math.sqrt(xDelta*xDelta + yDelta*yDelta));
            double force = Math.min(FORCEMAX,ATTRFORCECONSTANT*deltaLength);


            double dvx = (xDelta/deltaLength) * force;
            double dvy = (yDelta/deltaLength) * force;

            m_velocities.put(vertex1, new Point2D.Double(
                    m_velocities.get(vertex1).getX() + dvx,
                    m_velocities.get(vertex1).getY() + dvy));

            m_velocities.put(vertex2, new Point2D.Double(
                    m_velocities.get(vertex2).getX() - dvx,
                    m_velocities.get(vertex2).getY() - dvy));
            
        }
    }
    


    protected void activityStep(long elapsedTime) {
        super.activityStep(elapsedTime);
        
        Set<Widget> vertices = m_layout.vertexSet();
        Set<DefaultEdgeWithWidget> edges = m_layout.edgeSet();
        
        // Do multiple computational iterations for each visual update
        for ( int iter = 0; iter < ITERATIONS_PER_DRAW; ++iter ) {

            if (  m_energy > m_energyThreshold ) {
                // Apply damping
                for ( Widget vertex : vertices) {
                    double vx = m_velocities.get(vertex).getX() * DAMPING;
                    double vy = m_velocities.get(vertex).getY() * DAMPING;
                    m_velocities.put(vertex,new Point2D.Double(vx,vy));
                }


                Iterator outer = vertices.iterator();
                while (outer.hasNext()) {
                    Widget v1 = (Widget)outer.next();
                    boolean addRepulsion = false;
                    Iterator inner = vertices.iterator();
                    while ( inner.hasNext() ) {
                        Widget v2 = (Widget)inner.next();
                        // Add the overlaqp repulsion if we haven't
                        // added for this pair yet
                        if ( addRepulsion) {
                            calcRepulsion(v1,v2);
                        }
                        // Add repulsion from the next pair
                        if ( v1 == v2 ) {
                            addRepulsion = true;
                        }
                    }
                }


                for ( DefaultEdgeWithWidget edge : edges ) {
                    calcAttraction(m_layout.getEdgeSource(edge),m_layout.getEdgeTarget(edge));
                }
            }

            // Only run the overlap repulsion when the system is low in energy.
            // It has been observed that when this is done at the start of
            // the layout, it interferes with the layout and some vertices
            // become stuck. We actually only want the extra repulsion 
            // to only run at the end so that the vertices don't overlap
            // Empirically determined factor here
            boolean runOverlap = m_energy < m_energyThreshold*10;
//            if ( runOverlap ) {
//                Iterator outer = vertices.iterator();
//                while (outer.hasNext()) {
//                    Widget v1 = (Widget)outer.next();
//                    boolean addRepulsion = false;
//                    Iterator inner = vertices.iterator();
//                    while ( inner.hasNext() ) {
//                        Widget v2 = (Widget)inner.next();
//                        // Add the overlaqp repulsion if we haven't
//                        // added for this pair yet
//                        if ( addRepulsion) {
//                            calculateOverlapRepulsion(v1,v2);
//                        }
//                        // Add repulsion from the next pair
//                        if ( v1 == v2 ) {
//                            addRepulsion = true;
//                        }
//                    }
//                }
//            }

            // Calculate the current system energy
            m_energy = 0.0;
            for ( Point2D delta : m_velocities.values() ) {
                m_energy += delta.distanceSq(new Point2D.Double(0,0));
            }
            // The overlap ran, but the energy is still < the threshold,
            // now we can stop
            if ( runOverlap && ( m_energy < m_energyThreshold ) ) {
                terminate();
            }
            
            // Update the positions
            for ( Widget vertex : vertices ) {
                calcPositions(vertex);
            }

        }
        
        // Update the visual positions
        for ( Widget vertex : vertices ) {
            vertex.setOffset(m_positions.get(vertex));
        }
        
        // Update the edge widgets
        for ( DefaultEdgeWithWidget edge : edges ) {

            Line2D centerLine = new Line2D.Double(
                    m_layout.getEdgeSource(edge).getFullBounds().getCenter2D(),
                    m_layout.getEdgeTarget(edge).getFullBounds().getCenter2D());
            
            edge.getWidget().setOffset(0, 0);
            
            if ( ( m_layout.getEdgeSource(edge) instanceof HasEdgePort ) && 
                 ( m_layout.getEdgeTarget(edge) instanceof HasEdgePort ) ) { 
                edge.getWidget().setLine(new Line2D.Double(
                        ((HasEdgePort)m_layout.getEdgeSource(edge)).getPort(centerLine),
                        ((HasEdgePort)m_layout.getEdgeTarget(edge)).getPort(centerLine)));
            }

        }
    }
    
    
    // TODO: Replace with static finals
    // Easier to do it like this when doing debugging,
    // as these values will be loaded automatically into
    // the image beign debugged
    private double EPSILON = 0.000001D;
    private double ATTRFORCECONSTANT = 0.01;
    private double REPULFORCECONSTANT = 8.0;
    //private double OVERLAPREPULFORCECONSTANT = 30;
    private double MAXDELTA = 100.0;
    private double FORCEMAX = 100;
    private double ENERGY_THRESHOLD_PER_VERTEX = 0.5;
    //private double X_SKEW = 2.0;
    private double TIMESTEP = 0.1;
    private double DAMPING = 0.8;
    private double ITERATIONS_PER_DRAW = 20;
    private double VERTEX_PADDING = 30;

    // Keep positions independant of the visual rep, so we can do layout
    // in between visual updates by not using the visual position
    private Map<Widget,Point2D> m_positions = new HashMap<Widget,Point2D>();
    private Map<Widget,Point2D> m_velocities = new HashMap<Widget,Point2D>();
    
    private double m_energy = Double.MAX_VALUE;
    private double m_energyThreshold = 0;
    private GraphLayout m_layout;
}